
package com.trydofor.id.byta;

import java.io.UnsupportedEncodingException;

import com.trydofor.aloe.exception.FakeException;
import com.trydofor.id.a9ic.A9IC;
import com.trydofor.id.a9id.A9ID;

/**
 * the converter of byte array and long/int.
 * the byte order is bigEndian.
 * 
 * @author : Shi Rongjiu (www.trydofor.com)
 */
public final class Bytes {
    
    /**
     * default encoding
     */
    public static final String  ENCODING    = "UTF8";
    /**
     * the bytes of long
     */
    public static final int     BYTES_LONG  = 8;
    /**
     * the bytes of integer
     */
    public static final int     BYTES_INT   = 4;
    
    private static final int    BITS_BYTE   = 8;
    private static final byte[] EMPTY_BYTES = new byte[] {};
    private static final int    BYTE_MAST   = 0xFF;
    
    private Bytes() {
    }
    
    /**
     * get 8 bytes by a9id.
     * 
     * @param id a9id
     * @return byte[8], empty array if a9id is null.
     */
    public static byte[] toBytes(A9ID id) {
        if (id == null) return EMPTY_BYTES;
        return toBytes(id.longValue());
    }
    
    /**
     * fill bytes with 8 byte of a9id value.
     * the result is filled all or nothing.
     * 
     * @param bytes the bytes to fill
     * @param offset the first byte to fill
     * @param id the a9id value
     * @return filled bytes. Must 0 or 8
     */
    public static int fill(byte[] bytes, int offset, A9ID id) {
        if (id == null) return 0;
        return fill(bytes, offset, id.longValue());
    }
    
    /**
     * get a9id by 1-8 bytes.
     * 
     * @param bytes the source bytes
     * @param offset the first byte to use, SHOULD range [0,bs.length-1]
     * @param length the count of bytes to use, SHOULD range [1,8]
     * @return a9id
     */
    public static A9ID toA9ID(byte[] bytes, int offset, int length) {
        return A9ID.of(toLong(bytes, offset, length));
    }
    
    /**
     * get 8 bytes by a9ic.
     * 
     * @param ic a9ic
     * @return byte[8]], empty array if a9ic is null.
     */
    public static byte[] toBytes(A9IC ic) {
        if (ic == null) return EMPTY_BYTES;
        return toBytes(ic.longValue());
    }
    
    /**
     * fill bytes with 8 byte of a9ic value.
     * the result is filled all or nothing.
     * 
     * @param bytes the bytes to fill
     * @param offset the first byte to fill
     * @param ic the a9ic value
     * @return filled bytes. Must 0 or 8
     */
    public static int fill(byte[] bytes, int offset, A9IC ic) {
        if (ic == null) return 0;
        return fill(bytes, offset, ic.longValue());
    }
    
    /**
     * get a9ic by 1-8 bytes.
     * 
     * @param bytes the source bytes
     * @param offset the first byte to use, SHOULD range [0,bs.length-1]
     * @param length the count of bytes to use, SHOULD range [1,8]
     * @return a9ic
     */
    public static A9IC toA9IC(byte[] bytes, int offset, int length) {
        return A9IC.of(toLong(bytes, offset, length));
    }
    
    /**
     * get 8 bytes by long.
     * 
     * @param val the long.
     * @return byte[8]
     */
    public static byte[] toBytes(long val) {
        byte[] result = new byte[BYTES_LONG];
        fill(result, 0, val);
        return result;
    }
    
    /**
     * fill bytes with 8 byte of long value.
     * the result is filled all or nothing.
     * 
     * @param bytes the bytes to fill
     * @param offset the first byte to fill
     * @param val the long value
     * @return filled bytes. Must 0 or 8
     */
    public static int fill(byte[] bytes, int offset, long val) {
        if (bytes == null || bytes.length - offset < BYTES_LONG) return 0;
        for (int i = 0; i < BYTES_LONG; i++) {
            bytes[offset + i] = (byte) (val >>> (BITS_BYTE * (BYTES_LONG - 1 - i)));
        }
        return BYTES_LONG;
    }
    
    /**
     * get long by 1-8 bytes.
     * 
     * @param bytes the source bytes
     * @param offset the first byte to use, SHOULD range [0,bs.length-1]
     * @param length the count of bytes to use, SHOULD range [1,8]
     * @return long
     */
    public static long toLong(byte[] bytes, int offset, int length) {
        long result = 0L | bytes[offset];
        for (int i = 1; i < length; i++) {
            result = (result << BITS_BYTE) | (bytes[offset + i] & BYTE_MAST);
        }
        return result;
    }
    
    /**
     * get 4 bytes by int.
     * 
     * @param val the long.
     * @return byte[4]
     */
    public static byte[] toBytes(int val) {
        byte[] result = new byte[BYTES_INT];
        fill(result, 0, val);
        return result;
    }
    
    /**
     * fill bytes with 4 byte of int value.
     * the result is filled all or nothing.
     * 
     * @param bytes the bytes to fill
     * @param offset the first byte to fill
     * @param val the int value
     * @return filled bytes. Must 0 or 4
     */
    public static int fill(byte[] bytes, int offset, int val) {
        if (bytes == null || bytes.length - offset < BYTES_INT) return 0;
        for (int i = 0; i < BYTES_INT; i++) {
            bytes[offset + i] = (byte) (val >> (BITS_BYTE * (BYTES_INT - 1 - i)));
        }
        return BYTES_INT;
    }
    
    /**
     * get int by 1-4 bytes.
     * 
     * @param bytes the source bytes
     * @param offset the first byte to use, SHOULD range [0,bs.length-1]
     * @param length the count of bytes to use, SHOULD range [1,4]
     * @return long
     */
    public static int toInt(byte[] bytes, int offset, int length) {
        int result = 0 | bytes[offset];
        for (int i = 1; i < length; i++) {
            result = (result << BITS_BYTE) | (bytes[offset + i] & BYTE_MAST);
        }
        return result;
    }
    
    /**
     * get bytes of string and it's byte size.
     * 
     * <pre>
     * byte[] result
     * if (string != null){
     *      byte[] strs = string.getBytes(ENCODING);
     *      byte[] meta = toBytes(bytes.length);
     *      result = [meta,strs];
     * }else{
     *      result = toBytes(-1);
     * }
     * return result
     * </pre>
     * 
     * @param string the string
     * @return bytes of meta and string
     * @see #ENCODING
     */
    public static byte[] toMetaBytes(String string) {
        if (string == null) return toBytes(Integer.MIN_VALUE);
        
        try {
            byte[] bytes = string.getBytes(ENCODING);
            byte[] result = new byte[bytes.length + BYTES_INT];
            fill(result, 0, bytes.length);
            System.arraycopy(bytes, 0, result, BYTES_INT, bytes.length);
            return result;
        }
        catch (UnsupportedEncodingException e) {
            throw FakeException.wrap(e);
        }
    }
    
    /**
     * get the byte meta
     * 
     * @param bytes the source bytes
     * @param offset the first byte to use, SHOULD range [0,bs.length-1]
     * @return the byte meta
     */
    public static ByteMeta<String> ofMetaBytes(byte[] bytes, int offset) {
        String inst = null;
        int size = 0;
        if (bytes != null && bytes.length >= BYTES_INT) {
            int len = Bytes.toInt(bytes, offset, BYTES_INT);
            if (len == 0) {
                inst = "";
                size = BYTES_INT;
            }
            else if (len == Integer.MIN_VALUE) {
                size = BYTES_INT;
            }
            else if (len > 0 && bytes.length - offset - BYTES_INT >= len) {
                try {
                    inst = new String(bytes, offset + BYTES_INT, len, ENCODING);
                    size = BYTES_INT + len;
                }
                catch (UnsupportedEncodingException e) {
                    throw FakeException.wrap(e);
                }
            }
        }
        
        return ByteMeta.of(inst, size);
    }
    
    /**
     * Returns a hash code for the class. The hash code is computed as String (clzz.getCanonicalName())
     * 
     * @see String#hashCode()
     * @param claz the class to hash
     * @return a hash code value for the class, 0 if null.
     */
    public static int classType(Class<?> claz) {
        if (claz == null) return 0;
        
        int result = 0;
        char[] chars = claz.getCanonicalName().toCharArray();
        for (int i = 0; i < chars.length; i++) {
            result = 31 * result + chars[i];
        }
        return result;
    }
    
    /**
     * check the parameters of bytes
     * 
     * @param bytes the source
     * @param offset the offset
     * @param length the length
     * @return true if invalid
     */
    public static boolean invalidParam(byte[] bytes, int offset, int length) {
        if (bytes == null || offset < 0 || length < 0) return true;
        if (bytes.length - offset < length) return true;
        return false;
    }
    
    /**
     * check the parameters of bytes
     * 
     * @param bytes the source
     * @param offset the offset
     * @return true if invalid
     */
    public static boolean invalidParam(byte[] bytes, int offset) {
        return (bytes == null || offset < 0);
    }
}
